iT邦幫忙

2025 iThome 鐵人賽

DAY 29
0

🪞前言:資料啊資料,你到底藏了什麼

欸,今天我們不講 AI,不講 prompt,也不講「如何讓 LLM 幫你賺第一桶金」。
我們要講一個更古老、更穩定、也更容易讓你懷疑人生的東西:SQL。

你以為資料分析師在做什麼?
大部分時間,他們都在對資料庫講情話。
講到一半出現 error,接著開始罵人,罵完又回來改 query。
最後那句「GROUP BY」不只是 SQL 語法,而是工程師對人生的註解:
——「把一切都分組起來,這樣崩潰起來才有條理。」


🏰 我們的資料王國 schema

目前 schema

我們的世界觀長這樣 👇
四張表,像四個國家,各自有自己的性格、煩惱、和奇怪的外掛。

表名 角色定位
papers 論文資料庫,知識的倉庫,裡面塞滿 PDF、作者名,還有那種一句看不懂的標題
user_sent_papers Email pipeline 發送紀錄——你的論文快遞員
chat_history 對話紀錄。AI 和人互相傷害的證據。
user_setting 使用者設定。某種程度上,就是「人類情緒的 config 檔」。

如果用人格化比喻:

  • papers:學霸。
  • chat_history:話多又愛抱怨的人。
  • user_setting:情緒化主管。
  • user_sent_papers:默默幹活的實習生。

這四個湊在一起,像 Slack 裡那種奇怪的工作小組,吵歸吵,但少一個都不行。

schema 總覽

  • 共四張表
    • paper table : arxix pipeline 產生的論文資料庫,你懂的,知識來源
    • user_sent_paper : email pipeline 發送紀錄,你的論文快遞小幫手
    • chat_history : rag pipeline 的對話紀錄,AI 與人的腦力激盪
    • user_setting : 前端 產生的設定,管理使用者的喜怒哀樂

先閉上眼睛想一下,如果這些資料變得有趣,你就有成為資料分析師的潛力了。
如果不有趣…嗯,也讓我們繼續看下去 ☕️

(我知道這段你可能會想跳過,但拜託看一下,這就是我們分析的世界地圖。)

CREATE TABLE users (
    id VARCHAR(255) PRIMARY KEY,
    last_query_date DATE,
    total_queries INTEGER NOT NULL DEFAULT 0,
    remaining_tokens INTEGER NOT NULL DEFAULT 1000
);

CREATE TABLE user_setting (
    user_id VARCHAR(255) PRIMARY KEY,
    user_language VARCHAR(255) NOT NULL,
    translate BOOLEAN NOT NULL DEFAULT FALSE,
    system_prompt TEXT NOT NULL DEFAULT '',
    top_k INTEGER NOT NULL DEFAULT 5,
    use_rag BOOLEAN NOT NULL DEFAULT TRUE,
    subscribe_email BOOLEAN NOT NULL DEFAULT FALSE,
    reranker_enabled BOOLEAN NOT NULL DEFAULT TRUE,
    temperature FLOAT NOT NULL DEFAULT 0.6, -- LLM temperature
    CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);

CREATE TABLE chat_history (
    id SERIAL PRIMARY KEY,
    user_id VARCHAR(255) NOT NULL,
    input TEXT NOT NULL,
    output TEXT NOT NULL,
    input_token INT,
    output_token INT,
    latency_ms INT, -- 延遲時間
    model VARCHAR(64),
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);


CREATE TABLE papers (
    id SERIAL PRIMARY KEY,
    arxiv_id VARCHAR(32) UNIQUE NOT NULL,
    title TEXT NOT NULL,
    authors TEXT [] NOT NULL,
    abstract TEXT,
    categories TEXT [],
    published_date DATE,
    updated_date DATE,
    pdf_url TEXT,
    -- Parsed PDF content
    raw_text TEXT,
    sections JSON,
    "references" JSON,
    -- PDF processing metadata
    parser_used VARCHAR,
    parser_metadata JSON,
    pdf_processed BOOLEAN NOT NULL DEFAULT FALSE,
    pdf_processing_date TIMESTAMP,
    -- Timestamps
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);


CREATE TABLE user_sent_papers (
    id SERIAL PRIMARY KEY,
    user_id VARCHAR(255) NOT NULL,
    arxiv_id VARCHAR NOT NULL,
    sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

好,這一串如果你沒被嚇跑,恭喜,你比我還有毅力。
這就是一個「AI 助理平台」的資料後台骨架。
簡單說,它記錄了:

  • 誰在講話;
  • 講了什麼;
  • 生成速度多慢;
  • 哪些論文被寄出;
  • 哪些人懶得訂閱。

基本上,人生縮影。
——大家都在講話,但不是每個人都會回信。


🎯 單表分析(單挑模式)

單表分析就像一個人打副本。
沒有隊友,沒有救援,只有你和冷冰冰的資料庫。

但這樣也好,至少 SQL 不會偷你的午餐。

核心分析 SQL 範例
papers 論文分類統計 SELECT category, COUNT(*) FROM papers
user_sent_papers 每日發送量、每位使用者收到的論文數 GROUP BY day / user_id
chat_history 每位使用者總對話數、每日對話量、平均延遲與 token 使用量 GROUP BY user_id / day

🧪 papers:知識的墳場(我說倉庫)

  1. 論文分類統計
SELECT category, COUNT(*) AS paper_count
FROM papers, unnest(categories) AS category
GROUP BY category
ORDER BY paper_count DESC;
category paper_count
cs.AI 56
cs.LG 23
cs.CL 18

這結果很合理。AI 人寫最多的還是 AI。
就像設計師畫海報永遠加字體陰影一樣:明知道不該,但總是忍不住。

📬 user_sent_papers:Email 外送員的生死戰

  1. 每日發送數量
SELECT DATE(sent_at) AS day, COUNT(*) AS sent_count
FROM user_sent_papers
GROUP BY day
ORDER BY day;

day sent_count
2025-09-23 2
2025-09-24 3

五封信,感覺不多,但這世界上能把郵件寄出去又不被擋 spam 的人,已經值得敬禮。

  1. 每位使用者收到的論文數
SELECT user_id, COUNT(*) AS sent_paper_count
FROM user_sent_papers
GROUP BY user_id
ORDER BY sent_paper_count DESC;

user_id sent_paper_count
CCCCqBBCCDD85853WTWWNNNHJI22 5
gradio user 55

81 次對話,聽起來像「分手後還在找 AI 傾訴」的使用者。
AI:「你今天想聊什麼?」
使用者:「我前任…」
SQL:「SELECT * FROM trauma WHERE resolved = false;」

chat_history

  1. 每位使用者總對話數
SELECT user_id, COUNT(*) AS total_chats
FROM chat_history
GROUP BY user_id
ORDER BY total_chats DESC;

user_id total_chats
CCCCqBBCCDD85853WTWWNNNHJI22 81
gradio user 55
  1. 每日對話量
SELECT DATE(created_at) AS day, COUNT(*) AS chat_count
FROM chat_history
GROUP BY day
ORDER BY day;

day chat_count
2025-09-22 73
2025-09-23 61
2025-09-24 2
  1. 平均延遲與 token 使用量
SELECT 
    user_id,
    ROUND(AVG(latency_ms), 2) AS avg_latency_ms,
    ROUND(AVG(input_token), 2) AS avg_input_token,
    ROUND(AVG(output_token), 2) AS avg_output_token
FROM chat_history
GROUP BY user_id
ORDER BY avg_latency_ms DESC;
user_id avg_latency_ms avg_input_token avg_output_token
CCCCqBBCCDD85853WTWWNNNHJI22 4216.80 303.78 104.38
gradio user 1721.29 626.24 272.07

4200ms 平均延遲?
那不是延遲,那是修行。
在這期間,你可以泡咖啡、檢查人生選擇、順便懷疑自己是不是忘了加索引。


🤝 跨表分析(團戰模式)

單挑夠了,該組隊了。
跨表分析就像開會,一堆人講話但沒人聽懂對方。
SQL 就是那個冷靜的記錄員。

分析主題 SQL 目標 說明
使用者對話量 vs 收到論文數 LEFT JOIN chat_history + user_sent_papers 找出活躍使用者是否也收到大量論文
高互動但未訂閱 Email 過濾 COUNT(ch.id) > 50 & subscribe_email = FALSE 潛在促銷對象
RAG 功能使用情況 vs 對話量 chat_history + user_setting 了解高互動使用者是否有開 RAG
查詢論文是否被 Email 發送 chat_history + papers + user_sent_papers 了解 RAG pipeline 查詢結果轉化為 Email 的比例
被最多使用者查詢與發送的論文 papers + chat_history + user_sent_papers 找出最熱門的論文
使用者漏斗分析 users + chat_history + user_sent_papers + user_setting 從註冊 → 對話 → 收到論文 → 訂閱 Email 的流失分析
  1. 每位使用者對話量 vs 收到的論文數

可以找出:活躍聊天使用者是否也收到了大量論文。

SELECT
    u.id AS user_id,
    COUNT(DISTINCT ch.id) AS total_chats,
    COUNT(DISTINCT usp.arxiv_id) AS total_sent_papers
FROM users u
LEFT JOIN chat_history ch ON u.id = ch.user_id
LEFT JOIN user_sent_papers usp ON u.id = usp.user_id
GROUP BY u.id
ORDER BY total_chats DESC;

user_id total_chats total_sent_papers
lZCCqBoAJDM85JV3ZTWTDWNHije2 81 5
gradio user 55 0

這畫面很像辦公室裡那種人:
講最多的,文件最少。

  1. 🚫 高互動但未訂閱 Email 的使用者

找出高互動但沒有訂閱 Email 的潛在促銷對象。

SELECT
    u.id AS user_id,
    COUNT(ch.id) AS total_chats,
    COALESCE(SUM(CASE WHEN usp.id IS NOT NULL THEN 1 ELSE 0 END), 0) AS sent_count,
    us.subscribe_email
FROM users u
LEFT JOIN chat_history ch ON u.id = ch.user_id
LEFT JOIN user_sent_papers usp ON u.id = usp.user_id
LEFT JOIN user_setting us ON u.id = us.user_id
GROUP BY u.id, us.subscribe_email
HAVING COUNT(ch.id) > 50 AND (us.subscribe_email = FALSE OR us.subscribe_email IS NULL)
ORDER BY total_chats DESC;

user_id total_chats sent_count subscribe_email
gradio user 55 0

這種人現實裡也常見:
跟你聊超多,但從不 follow。
社群媒體叫這種人「已讀狂魔」,CRM 叫他「潛在客戶」。

  1. ⚙️ RAG 功能使用情況 vs 對話量

可以評估 RAG 功能是否被高互動使用者使用,幫助優化 pipeline。

SELECT
    u.id AS user_id,
    COUNT(ch.id) AS total_chats,
    us.use_rag,
    us.reranker_enabled
FROM users u
LEFT JOIN chat_history ch ON u.id = ch.user_id
LEFT JOIN user_setting us ON u.id = us.user_id
GROUP BY u.id, us.use_rag, us.reranker_enabled
ORDER BY total_chats DESC;

user_id total_chats use_rag reranker_enabled
lZCCqBoAJDM85JV3ZTWTDWNHije2 81 false false
gradio user 55 false false

兩位都沒開 RAG。
也就是說——我們花了一週做的功能,沒人用。
工程師的眼淚,掉進 NULL 裡。

  1. 🔍 查詢的論文是否被 Email 寄出

用來分析:使用者在 RAG pipeline 查詢的論文,有多少實際被發送 Email。

SELECT
    ch.user_id,
    ch.id AS chat_id,
    COALESCE(ARRAY_AGG(DISTINCT p.arxiv_id) FILTER (WHERE p.id IS NOT NULL), '{}') AS queried_papers,
    COALESCE(
        (SELECT ARRAY_AGG(usp.arxiv_id)
         FROM user_sent_papers usp
         WHERE usp.user_id = ch.user_id
           AND usp.arxiv_id IN (
               SELECT p2.arxiv_id
               FROM papers p2
               WHERE ch.input ILIKE '%' || p2.title || '%'
           )
        ), '{}'
    ) AS sent_papers
FROM chat_history ch
LEFT JOIN papers p ON ch.input ILIKE '%' || p.title || '%'
GROUP BY ch.user_id, ch.id
ORDER BY ch.user_id, ch.id;


user_id chat_id queried_papers sent_papers
gradio user 74 {2509.15151v1} {}
gradio user 75 {2509.15151v1} {}
gradio user 137 {2509.17998v1} {}
gradio user 138 {2509.17998v1} {}
lZCCqBoAJDM85JV3ZTWTDWNHije2 10 {} {}
lZCCqBoAJDM85JV3ZTWTDWNHije2 11 {} {}
lZCCqBoAJDM85JV3ZTWTDWNHije2 34 {} {}

查了,但沒寄。
有點像 Tinder:配對到了,沒講話。
資料分析有時候不殘酷,它只是誠實。

  1. 🌋 最熱門的論文

找出最受關注的論文,分析 RAG pipeline 與 Email pipeline 的重疊。

SELECT
    p.arxiv_id,
    p.title,
    COUNT(DISTINCT ch.user_id) AS queried_by_users,
    COUNT(DISTINCT usp.user_id) AS sent_to_users
FROM papers p
LEFT JOIN chat_history ch ON ch.input ILIKE '%' || p.title || '%'
LEFT JOIN user_sent_papers usp ON p.arxiv_id = usp.arxiv_id
GROUP BY p.arxiv_id, p.title
ORDER BY queried_by_users DESC, sent_to_users DESC
LIMIT 20;

arxiv_id title queried_by_users sent_to_users
2509.15057v1 Balancing Sparse RNNs with Hyperparameterization Benefiting¶ Meta-Learning 1 0
2509.15167v1 Semi-Supervised 3D Medical Segmentation from 2D Natural Images¶ Pretrained Model 1 0
2509.15151v1 Exploring How Audio Effects Alter Emotion with Foundation Models 1 0
2509.17998v1 Adaptive Kernel Design for Bayesian Optimization Is a Piece of CAKE with¶ LLMs 1 0
2509.15116v1 The mechanization of science illustrated by the Lean formalization of¶ the multi-graded Proj construction 0 1
2509.18046v1 HuMam: Humanoid Motion Control via End-to-End Deep Reinforcement¶ Learning with Mamba 0 1
2509.18060v1 TMD-TTS: A Unified Tibetan Multi-Dialect Text-to-Speech Synthesis for¶ Ü-Tsang, Amdo and Kham Speech Dataset Generation 0 1
2509.16179v1 Fast OTSU Thresholding Using Bisection Method 0 1
2509.15156v1 Leveraging Geometric Visual Illusions as Perceptual Inductive Biases for¶ Vision Models 0 1
2509.15174v1 SMARTER: A Data-efficient Framework to Improve Toxicity Detection with¶ Explanation via Self-augmenting Large Language Models 0 0

最熱門的一篇,也只有一個人查。
這平台的社交狀況,大概跟我一樣冷清。

  1. 🧮 使用者漏斗分析

看到「總使用者 → 對話使用者 → 收到論文 → 訂閱 Email」的流失情況

SELECT
    COUNT(*) AS total_users,
    COUNT(DISTINCT CASE WHEN has_chat THEN id END) AS chat_users,
    COUNT(DISTINCT CASE WHEN has_sent THEN id END) AS emailed_users,
    COUNT(DISTINCT CASE WHEN subscribe_email THEN id END) AS subscribed_users,
    ROUND(100.0 * COUNT(DISTINCT CASE WHEN has_chat THEN id END) / COUNT(*), 1)  || '%' AS pct_chat_users,
    ROUND(100.0 * COUNT(DISTINCT CASE WHEN has_sent THEN id END) / COUNT(*), 1)  || '%' AS pct_emailed_users,
    ROUND(100.0 * COUNT(DISTINCT CASE WHEN subscribe_email THEN id END) / COUNT(*), 1)  || '%' AS pct_subscribed_users
FROM (
    SELECT
        u.id,
        us.subscribe_email,
        EXISTS(SELECT 1 FROM chat_history ch WHERE ch.user_id = u.id) AS has_chat,
        EXISTS(SELECT 1 FROM user_sent_papers usp WHERE usp.user_id = u.id) AS has_sent
    FROM users u
    LEFT JOIN user_setting us ON u.id = us.user_id
) t;

total_users chat_users emailed_users subscribed_users pct_chat_users pct_emailed_users pct_subscribed_users
100 80 60 50 80% 60% 50%

80% 會聊天,60% 收信,50% 訂閱。
比我在交友軟體的轉換率高多了。


☕ 小結:SQL 不只是查資料,它是人生投影

寫 SQL 的過程,就像在面對現實。
你以為只是「JOIN 幾個表」,結果 JOIN 出來的,是人性。

有些人話多但不行動。
有些人行動快但不回頭。
有些人只是靜靜地存在 database 裡,從未被 select 過。

但這樣也好。
資料不會騙你,只會讓你更懂這個世界。
哪怕懂得越多,越想重灌人生。

📚 總結一句話:
SQL 就像感情——寫錯一句,結果全歪。
但寫對的那一刻,真的會有點魔法的感覺。


上一篇
Day28|生命線大公開!Grafana Dashboard 一眼看透系統健康
下一篇
Day29|🔥 不想再半夜救火?讓 Alertmanager 當你的預知系統!🔮
系列文
論文流浪記:我與AI 探索工具、組合流程、挑戰完整平台33
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言